package org.jugile.daims;

/*

Copyright (C) 2011-2011 Jukka Rahkonen  email: jukka.rahkonen@iki.fi

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

*/

import java.io.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.log4j.Logger;
import org.jugile.daims.anno.Connection1N;
import org.jugile.daims.anno.ConnectionNN;
import org.jugile.util.Buffer;
import org.jugile.util.DBConnection;
import org.jugile.util.Jugile;

/**
 * <i>"For this is what the LORD says — he who created the heavens, he is God; 
 * he who fashioned and made the earth, he founded it; 
 * he did not create it to be empty, but formed it to be inhabited — he says: 
 * 'I am the LORD, and there is no other.'"</i> <b>(Isaiah 45:18)</b>
 * 
 * <br/>==========<br/>
 *
 * here is doc
 *  
 * @author jukka.rahkonen@iki.fi
 *
 */
public class BoMapDelta<E extends Bo> extends Jugile {
	static Logger log = Logger.getLogger(BoMapDelta.class);

	private UnitOfWork uow() { return DomainCore.getUnitOfWork(); }

	protected Map<Long,E> items = new HashMap<Long,E>(2);
	protected Map<Long,E> removed = new HashMap<Long,E>(2);
	protected Map<Long,Object> props = null;

	protected void reset() {
		items = new HashMap<Long,E>(2);
		removed = new HashMap<Long,E>(2);
		props = null;
		size = 0;
	}
	
	protected BoMap<E> origin;
	public BoMapDelta(BoMap<E> origin) {
		this.origin = origin;
	}
	public BoMapDelta(Class<E> cl) {
		origin = new BoMap<E>(cl,null);
	}
	
	public Object getProp(long id) { 
		if (props == null) return null;
		return props.get(id);
	}
	public Object getProp(E o) { 
		if (o == null) return null;
		return getProp(o.id());
	}
	
	public final void remove(E o) { remove(o,true); }
	public final void remove(E o, boolean cleanup) {
		if (o == null) return;
		//log.debug("remove: " + o);
		if (contains(o.id())) {
			//log.debug("  it contains it");
			// contains() test is needed to keep correct size count
			if (!removed.containsKey(o.id())) {
				// NOTE: do not touch items here: it causes concurrent modification exception
				removed.put(o.id(),o);
				size--;
				if (cleanup) try { o.cleanUpConnections(); } catch (Exception e) {fail(e);}
				if (props != null) props.remove(o.id());
			}
		} else {
			//log.debug("  it didnt contain it");
		}
	}
	
	
//	protected void removeAll() {
//		log.debug("removeAll");
//		for (E o : (BoCollection<E>)this) {
//			log.debug("  removing: " + o);
//			remove(o,false);
//		}
//	}
	
	public final boolean contains(long id) {
		if (removed.containsKey(id)) return false;
		if (items.containsKey(id)) return true;
		if (origin.contains(id)) return true;
		return false;
	}
	
	
	private int size = 0;
	
	public final void add(E o) { add(o,null); }
	public final void add(E o, Object vo) {
		if (o == null) return;
		// remove removed
		if (removed.remove(o.id()) != null) {
			//log.debug("add(o) removed: " + o);
			if (items.containsKey(o.id())) size++; // for case of re-adding
		}
		if (!items.containsKey(o.id())) {
			//log.debug("add: " + o);
			items.put(o.id(),o);
			size++;
			if (vo != null) {
				if (props == null) props = new HashMap<Long,Object>();
				props.put(o.id(), vo);
			}
		}
	}
	
	public final void add(List<E> lst) { for (E o : lst) add(o); }
	//public final void add(BoMapDelta<E> map) { for (E o : map) add(o); }

	public final int getSize() {
		return origin.size() + size;
	}
	
	// allways gets a copy of original or new from delta
	public final E get(long id) {
		if (removed.containsKey(id)) return null;
		E o = items.get(id);
		if (o != null && !o.isRef()) { // check if this is lazyloaded reference
			return o;
		}
		o = origin.getCopy(id);
		if (o == null) {
			//fail("original object not found: " + origin.getClazz().getName() + ": " + id);
			// empty collections ( new BoCollection(); ) in app have no original items
			return null;
		}
		items.put(o.id(),o);
		return o;
	}
	
	protected final E createNewBo() {
		E o = (E)Bo.createNew(origin.getClazz());
		items.put(o.id(),o);
		size++;
		return o;
	}
	
	protected final boolean isRemoved(E o) {
		if (removed.containsKey(o.id())) return true;
		return false;
	}
	
	public class BoMapIterator implements Iterator<E> {
		BoMapDelta<E> bm = null;
		Iterator<E> i = null;
		Iterator<E> oi = null;
		public BoMapIterator(BoMapDelta<E> bm) {
			this.bm = bm;
			i = bm.items.values().iterator();
			if (bm.origin != null) {
				oi = bm.origin.iterator();
			}
			next = getNext(); // find first next
		}
		
		private E next = null;
		public E getNext() {
			// iterate own items
			while (i.hasNext()) {
				E o = i.next();
				// is it removed
				if (!removed.containsKey(o.id())) {
					return o;
				}
			}
			
			// iterate origin items
			while (oi.hasNext()) {
				E o = oi.next();
				if (removed.containsKey(o.id())) {
					continue;
				}
				if (items.containsKey(o.id())) {
					continue; // allready visited
				}
				return o;
			}
			return null; // not found, end reached
		}
		
		public boolean hasNext() { return next != null; }
		public E next() {
			E res = next;
			next = getNext();
			if (!uow().isReadOnly())
				if (res != null && res.isOrigin()) res = (E)res.getUowCopy();
			return res;
		}

		public void remove() { fail("remove not supported");}		
	}

	// no iterator() named method for avoiding accidentaly using this iterator
	// instead of inherited BoCollection's iterator. 
	// TODO: change this name later back to iterator() when everything works.
	public final Iterator<E> deltaIterator() {
		return new BoMapIterator(this);
	}
	
	/**
	 * Locally remove added / modified
	 */
	private void localRemove() {
		Iterator<Long> i = removed.keySet().iterator();
		while (i.hasNext()) {
			long id = i.next();
			Bo o = items.remove(id);
			if (o != null)
				if (o.isNew()) // only new items should be removed from delete list
					i.remove();
		}
	}
	

	public int getCommitSize() {
		int cs = removed.size();
		cs += items.size();
		return cs;
	}
	
	public void delta(Writer out) throws Exception {
		if (getCommitSize() == 0) return; // empty set
		
		// header row
		//log.debug("delta. first: " + _first());
		out.write(_first()._headerRow() + "\n");
		
		// 1. add added and merge modified
		Iterator<E> i2 = items.values().iterator();
		while (i2.hasNext()) {
			E o = i2.next();
			if (o.isModified() || o.isNew()) 
				out.write(o._delta() + "\n");
		}
		
		// TODO: local cleanup: remove locally removed
		// 2. remove deleted ( if same transaction creates and deletes)
		Iterator<E> i = removed.values().iterator();
		while (i.hasNext()) {
			E o = i.next();
			out.write(o._deleteRow() + "\n");
		}
	}
	
	
	// TODO: clean up - combine with BoMap.nncsv()
	public void nndelta(Writer out) throws Exception {
		if (getCommitSize() == 0) return; // empty set
		//log.debug("nndelta. first: " + _first());
		for (Field f : bi().nns) {
			String cn1 = f.getName();
			print("====== :" +cn1);
			// iterate all items and add changes on n-n collection
			if (items.size() > 0) {
				out.write(_first()._nnHeader(f) + "\n");
				Iterator<E> i2 = items.values().iterator();
				while (i2.hasNext()) {
					E o = i2.next();
					BoMapDelta<E> m = (BoMapDelta<E>)o.getBoCollection(cn1);
					List<E> localItems = m.getLocalItems();
					if (localItems.size() > 0) {
						out.write(((Bo)o).getId());
						for (Bo bo : localItems) {
							// items() of n-n collection, added
							out.write(Bo.CSVDELIMITER + bo.getId());
						}
						out.write("\n");						
					}
					List<E> deletedItems = m.getDeletedItems();
					if (deletedItems.size() > 0) {
						out.write(Bo.MAPSTART+"D"+Bo.CSVDELIMITER+((Bo)o).getId());
						for (Bo bo : deletedItems) {
							// items() of n-n collection, added
							out.write(Bo.CSVDELIMITER + bo.getId());
						}
						out.write("\n");						
					}
				}
			}
		}
	}

	private E _first = null;
	private E _first() {
		// instantiate
		if (_first != null) return _first;
		try { 
			_first = origin.getClazz().newInstance(); 
			return _first;
		} catch (Exception e) { fail(e); return null; }
	}
	protected E getProto() { return _first(); }

	private List<E> getLocalItems() {
		List<E> res = new ArrayList<E>();
		Iterator<E> i = items.values().iterator();
		while (i.hasNext()) {
			E o = i.next();
			res.add(o);
		}
		return res;
	}

	private List<E> getDeletedItems() {
		List<E> res = new ArrayList<E>();
		Iterator<E> i = removed.values().iterator();
		while (i.hasNext()) {
			E o = i.next();
			res.add(o);
		}
		return res;
	}

	private BoInfo bi;
	protected BoInfo bi() {
		if (bi == null) bi = _first().bi();
		return bi;
	}
	
	protected int writeToDB(DBConnection c) throws Exception {
		if (getCommitSize() == 0) return 0; // empty set
		int count = 0;
		
		String table = bi().table;
		
		// local cleanup - remove removed
		localRemove();
		
		// remove deleted
		//print("============ writeToDB: " + this);
		Iterator<E> i3 = removed.values().iterator();
		while (i3.hasNext()) {
			E o = i3.next();
			if (o.id() == 0 || o.version() <= 0) {
				log.warn("tried to remove ghost object: " + o);
				continue;
			}
			if (o.isArchived()) continue;
			c.prepare("delete from " + table + " where id_f=? AND vers=?");
			print("DELETE: " + o);
			//c.prepare("delete from " + table + " where id_f=?");
			c.param(o.id());
			c.param(o.version());
			//print("delete: " + o.id() + "," + o.version());
			int del = c.execute();
			if (del != 1) {
				fail("could not delete: " + o);
			}
			removeAllNN(c,o); // remove all n-n references from db
			count++;
		}
		
		// add added and merge modified
		Iterator<E> i2 = items.values().iterator();
		while (i2.hasNext()) {
			E o = i2.next();
			// handle n-n updates
			updateNN(c,o);
			if (o.isModified() || o.isNew()) {
				o._dbWriteFlds(c,bi());
				count++;
			}
		}			
		return count;
	}

	private void removeAllNN(DBConnection c, Bo o) throws Exception {
		// all n-n connections
		int i = 0;
		for (Field f : bi().nns) {
			String nntable = bi().nntables.get(i++);
			//c.prepare("delete from " + nntable + " where o1=?");
			c.prepare("delete from " + nntable + " where o2=?"); // ang swap
			c.param(o.id());
			c.execute();
		}
	}
	
	private void updateNN(DBConnection c, Bo o) throws Exception {
		// all n-n connections
		int i = 0;
		for (Field f : bi().nns) {
			String nntable = bi().nntables.get(i++);
			BoMapDelta<Bo> m = o.getBoCollection(f.getName());
			List<Bo> localItems = m.getLocalItems();
			if (localItems.size() > 0) {
				long oid1 = o.id();
				for (Bo bo : localItems) {
					// items() of n-n collection, added
					long oid2 = bo.id();
					c.prepare("replace into " + nntable + " set o1=?,o2=?");
					c.param(oid2); // swap needed to ang?
					c.param(oid1);
//					c.param(oid1);
//					c.param(oid2);
					c.execute();
				}
			}
			List<Bo> deletedItems = m.getDeletedItems();
			if (deletedItems.size() > 0) {
				long oid1 = o.id();
				for (Bo bo : deletedItems) {
					// items() of n-n collection, deleted
					long oid2 = bo.id();
					c.prepare("delete from " + nntable + " where o1=? AND o2=?");
					c.param(oid2); // swap needed to ang?
					c.param(oid1);
//					c.param(oid1);
//					c.param(oid2);
					c.execute();
				}
			}
		}
	}
	
	public String status() {
		Buffer buf = new Buffer();
		buf.nl();
		buf.ln("removed: " + removed.size());
		buf.ln("items: " + items.size());
		if (origin != null) {
			buf.ln("origin: " + origin.size());
		}
		return buf.toString();
	}

	public String toString() {
		String res = "[Delta " + getClassName(origin.getClazz()) + " " +
				hex(hashCode()) + " del:" + removed.size() + " items:" 
				+ items.size() + " origin:"+origin.size() + "] ";
		return res;
	}
	
	protected void dumpDeleted(Buffer buf) throws Exception {
		//		deleted: 
		//		    5430D082 Person
		//		    5430D082 Person
		//		    5430D082 Family
		buf.incrIndent();
		for (Bo o : this.getDeletedItems()) {
			buf.ln(o.toRefStr());
		}
		buf.decrIndent();
	}
	
	protected void dumpItems(Buffer buf) throws Exception {
		//		items:
		//		    5430D082 Person (2,1,0,-,p1,-)  ->  5430D082
		//		        family: 5430D082  Family (ref)
		//		        family: 5430D082  Family (2,1,0) ->  5430D082  
		buf.incrIndent();
		for (Bo o : this.getLocalItems()) {
			o.dump(buf);
			buf.ln();
		}
		buf.decrIndent();
	}

	protected void dumpShort(Buffer buf) throws Exception {
		//      persons( 5430D082 -> 5430D082 ):
		//          items: 5430D082, 5430D082, 5430D082, 5430D082
		//          deleted: 5430D082, 5430D082  
		buf.ind().add("items: ");
		int ind = 0;
		for (Bo o : getLocalItems()) {
			if (ind++ > 0) buf.add(", ");
			buf.add(o.realHash());
		}
		buf.nl();
		
		buf.ind().add("deleted: ");
		ind = 0;
		for (Bo o : getDeletedItems()) {
			if (ind++ > 0) buf.add(", ");
			buf.add(o.realHash());
		}
		buf.nl();
	}
	
	


}
